package com.wx.lock.redis;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.UUID;

/**
 * @author wx
 * @Description: 分布式锁
 * @date 2018/12/1
 */
public class DistributedLock {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    private static final Long RELEASE_SUCCESS = 1L;
    private final JedisPool jedisPool;

    public DistributedLock(JedisPool jedisPool) {
        this.jedisPool = jedisPool;
    }

    /**
     * @param locaName       锁名
     * @param acquireTimeout 获取锁的超时时间
     * @param timeout        超时时间
     * @methodName: lockWithTimeout
     * @author: wx
     * @description: 获取锁
     * @date: 2018/12/1
     * @return: java.lang.String
     */
    public String lockWithTimeout(String locaName, long acquireTimeout, long timeout) {
        // jedis连接
        Jedis connJedis = null;
        // 储存锁的vlaue值
        String reIdentifier = null;
        try {
            // 获取连接
            connJedis = jedisPool.getResource();
            // 随机生成锁的value
            String identifier = UUID.randomUUID().toString();
            // 锁名，即key值
            String lockKey = "lock:" + locaName;
            // 超时时间，上锁后超过此时间则自动释放锁
            int lockExpire = (int) (timeout / 1000);
            // 获取锁的超时时间，超过这个时间则放弃获取锁
            long end = System.currentTimeMillis() + acquireTimeout;

            while (System.currentTimeMillis() < end) {
                /**
                 * 设置key与设置超时时间变为原子操作
                 * NX = 即当key不存在时，我们进行set操作；若key已经存在，则不做任何操作
                 * PX = 意思是我们要给这个key加一个过期的设置
                 */
                String result = connJedis.set(lockKey, identifier, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, lockExpire);
                if (Objects.equals(LOCK_SUCCESS, result)) {
                    reIdentifier = identifier;
                }
            }
        } finally {
            if (connJedis != null) {
                connJedis.close();
            }
        }
        return reIdentifier;
    }

    /**
     * @methodName: releaseLock
     * @author: wx
     * @description: 释放锁
     * @param lockName 锁的后缀名
     * @param identifier 锁的value
     * @date: 2018/12/1
     * @return: boolean
     */
    public boolean releaseLock(String lockName, String identifier) {
        Jedis conn = null;
        String lockKey = "lock:" + lockName;
        try {
            /**
             * 关于为什么使用evla的说明
             * 问题在于如果调用jedis.del()方法的时候，这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景？
             * 答案是肯定的，比如客户端A加锁，一段时间之后客户端A解锁，在执行jedis.del()之前，锁突然过期了，此时客户端B尝试加锁成功，
             * 然后客户端A再执行del()方法，则将客户端B的锁给解除了
             */
            
            conn = jedisPool.getResource();
            //Lua代码的功能是什么呢？其实很简单，首先获取锁对应的value值，检查是否与requestId相等，如果相等则删除锁（解锁）
            String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
            //eval()方法可以确保原子性
            Object result = conn.eval(script, Collections.singletonList(lockKey), Collections.singletonList(identifier));
            if (RELEASE_SUCCESS.equals(result)) {
                return true;
            }
            return false;
        } finally {
            if (conn != null) {
                conn.close();
            }
        }
    }
}
